package ca.uvic.group2.server;

import ca.uvic.group2.ServerFactory;
import ca.uvic.group2.api.*;
import ca.uvic.group2.api.exceptions.*;
import java.io.*;
import java.math.*;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.sql.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.*;
import java.net.*;
import org.apache.log4j.*;



public class DBServerImpl2 extends UnicastRemoteObject implements DBServer, RemoteServer, StatsCollector {

    public static final int NUM_STOCK_TABLES = 2;

    private static Category debugLog = Logger.getInstance("DBServDebug");
    private static Category perfLog = Logger.getInstance("DBServPerf");

    private String _serverName = "DBServer2";
    private Hashtable users = new Hashtable();
    private Hashtable buyTriggers = new Hashtable();
    private Hashtable sellTriggers = new Hashtable();
    private int triggerId = 0;
    private AuditLog logger;
    private String serverId = "DBServer";
    private ThreadPoolExecutor threadPool;
    private BlockingQueue<Runnable> taskQueue;
    private Stats myStats = new Stats(Stats.DB, "DBServer");
    private ReindexThread reindexThread = new ReindexThread();
    
    class ReindexThread extends Thread {
	private boolean terminated = false;
	private int count = 0;
	
	public ReindexThread() {
	}

	private void reindex(DBConnection2 db) {
	    debugLog.info("REINDEX Started");
	    try {
		db.execUpdate("REINDEX TABLE users;");
		db.execUpdate("REINDEX TABLE dollaraccounts;");
		db.execUpdate("REINDEX TABLE stockaccounts0;");
		db.execUpdate("REINDEX TABLE stockaccounts1;");
	    } catch (Exception err) {
		debugLog.error("REINDX ERROR", err);
	    }
	    debugLog.info("REINDEX complete");
	}
	
	public void terminate() {
	    terminated = true;
	}

	public void run() {
	    try {
		debugLog.info("REINDEX thread started");
		DBConnection2 db = new DBConnection2();
		db.openConnection();
		while (!terminated) {
		    try {
			Thread.sleep(1000);
			if (++count % 30 == 0) {
			    reindex(db);
			}
		    } catch (InterruptedException ignored) {
		    }
		    
		}
		db.closeConnection();
	    } catch (SQLException err) {
		debugLog.fatal("REINDEX Thread failed to start: " + err.getMessage(), err);
	    }
	    debugLog.info("REINDEX thread stopped");
	}
    }

    public DBServerImpl2() throws RemoteException, CommException {
        super();
        logger = new ServerFactory().getAuditServer();

        int poolSize = 10;
        try {
            poolSize = Integer.parseInt(System.getProperty("group2.db.poolSize", "10"));
        } catch (Exception err) {
            debugLog.error("Error parsing pool size", err);
        }
        debugLog.info("Pool size is " + String.valueOf(poolSize));


        threadPool = new ThreadPoolExecutor(poolSize,  // Minimum 10 Threads in pool
                                            poolSize,  // Maximum 10 Threads in pool
                                            1000, TimeUnit.SECONDS,  // Threads closed after 1000 seconds of inactivity
                                            new LinkedBlockingQueue(1000)); // Maximum Queue size
        taskQueue = threadPool.getQueue();

    }

    //
    // RemoteServer interface
    //
    private int _port;
    public void serverStart(int port) throws RemoteException, CommException {
        try {
            ServerFactory.serverStart(this, port, RMI_NAME);
            _port = port;

	    reindexThread.start();
        } catch (Exception err) {
            err.printStackTrace();
            throw new CommException(err.getMessage());
        }
    }

    public void serverStop() throws RemoteException, CommException {
        try {
            debugLog.info("DBServer received stop signal.  Cleaning up...");
            debugLog.info("DBServer shutting down thread pool...");
            threadPool.shutdown();
            threadPool.awaitTermination(20, TimeUnit.SECONDS);

	    debugLog.info("Stopping REINDEX thread...");
	    reindexThread.terminate();
	    try {
		reindexThread.wait(10000);
	    } catch (InterruptedException ignored) {
	    }
	    debugLog.info("REINDEX thread Stopped.");

            debugLog.info("DBServer closing all connections...");
            DBConnectionPool.getInstance().closeAllConnections();

            debugLog.info("DBServer shutdown complete.");
        } catch (Exception err) {
            err.printStackTrace();
            throw new CommException(err.getMessage());
        }
    }


    public void setServerName(String name) throws RemoteException {
        _serverName = name;
        myStats.name = name;
    }

    public String getServerName() throws RemoteException {
        return _serverName;
    }

    public String getHostName() throws RemoteException {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException err) {
            return null;
        }
    }

    //
    //
    //


    public void initDatabase() throws CommException, RemoteException {
        debugLog.info("init:start");

        //drop and re-add the tables to the database
        String qry_drop = "", qry_createUserTbl = "", qry_createDollarActTbl = "", qry_createStockActTbl = "", qry_createTriggerTbl = "", qry_createIndexes = "";
        int returnValDrop = -1, returnValCreate = -1;

        debugLog.info("init: execute drop");

        try {
            returnValDrop = exec_update("DROP TABLE users;");
        } catch (SQLException err) {
            debugLog.warn("Drop 'users' failure: " + err.getMessage());
        }
        try {
            returnValDrop = exec_update("DROP TABLE dollaraccounts");
        } catch (SQLException err) {
            debugLog.warn("Drop 'dollaraccounts' failure: " + err.getMessage());
        }
        try {
            returnValDrop = exec_update("DROP TABLE triggers");
        } catch (SQLException err) {
            debugLog.warn("Drop 'triggers' failure: " + err.getMessage());
        }

        try {
            for (int i = 0; i < NUM_STOCK_TABLES; ++i) {
                returnValDrop = exec_update(DBSetup.dropStockTable(i));
            }
        }
        catch (SQLException err) {
            debugLog.warn("Drop stockaccount tables failure: " + err.getMessage());
        }

        debugLog.info("init: execute create");

        try {
            returnValCreate = exec_update(DBSetup.createUsersTbl);
        } catch (SQLException err) {
            debugLog.error("Create 'users' error: " + err.getMessage());
        }

        try {
            returnValCreate = exec_update(DBSetup.createDollarActsTbl);
        } catch (SQLException err) {
            debugLog.error("Create 'dollaraccounts' error: " + err.getMessage());
        }

        for (int i = 0; i < NUM_STOCK_TABLES; ++i) {
            try {
                returnValCreate = exec_update(DBSetup.createStockTable(i));
            } catch (SQLException err) {
                debugLog.error("Create 'stockaccounts' error: " + err.getMessage());
            }
        }

        try {
            returnValCreate = exec_update(DBSetup.createTriggersTbl);
        } catch (SQLException err) {
            debugLog.error("Create 'triggers' error: " + err.getMessage());
        }

        debugLog.info("init: done all queries");

        this.beginRun();
        debugLog.info("init: end");

    }

    public int login(String username) throws LoginException, CommException, RemoteException {

        debugLog.info("DB Login");

        //
        // Create and execute the login task
        //
        LoginDBTask dbTask = new LoginDBTask(username);
        long startTime = System.currentTimeMillis();
        synchronized (dbTask) {

            // execute() method returns when the task has been added to the queue

            this.threadPool.execute(dbTask);
            long queueSize = this.taskQueue.size();
            perfLog.info("Queue Size: " + queueSize);
            myStats.addQueueSize(queueSize);

            // Wait for the task to complete
            try {
                dbTask.wait();
                addStats(startTime, dbTask.getReadQryCount(), dbTask.getWriteQryCount(),
                         dbTask.getQueueTime(), dbTask.getExeTime());
            } catch (InterruptedException ignored) {
            }
        }

        // If an error occured, throw the exception.
        dbTask.throwIfError();
        dbTask.throwIfLoginError();

        // Otherwise, get the userinfo and userid.
        int userid = dbTask.getUserId();
        UserInfo userInfo = dbTask.getUserInfo();
        debugLog.info("Login user id: " + Integer.toString(userid));

        // Store the userinfo and return the user id.
        users.put(new Integer(userid), userInfo);
        return userid;

    }

    private UserInfo findUserInfo(int userId) throws CommException {
        Integer key = new Integer(userId);
        if (users.containsKey(key)) {
            return (UserInfo)users.get(key);
        }
        throw new CommException("Userid not found");
    }

    public UserInfo getUserInfo(int userId) throws CommException {
        return new UserInfo(findUserInfo(userId));
    }

    public Receipt buy(int transId, int userId, PurchaseOrder order) throws CommException, RemoteException {
       debugLog.info("DB Buy");

       UserInfo userInfo = findUserInfo(userId);

       //
       // Create and execute the buy task
       //
       BuyDBTask dbTask = new BuyDBTask(transId, userId, order, userInfo);
       long startTime = System.currentTimeMillis();
       synchronized (dbTask) {

           // execute() method returns when the task has been added to the queue
           this.threadPool.execute(dbTask);
           long queueSize = this.taskQueue.size();
           perfLog.info("Queue Size: " + queueSize);
           myStats.addQueueSize(queueSize);

           // Wait for the task to complete
           try {
               dbTask.wait();
               addStats(startTime, dbTask.getReadQryCount(), dbTask.getWriteQryCount(),
                        dbTask.getQueueTime(), dbTask.getExeTime());
           } catch (InterruptedException ignored) {
           }
       }

       // If an error occured, throw the exception.
       dbTask.throwIfError();

       // Otherwise, get the receipt
       Receipt r = dbTask.getReceipt();
       debugLog.info("Buy complete");

       logger.accountRemove(serverId, transId, userInfo.username, order.total.doubleValue());

       // return the buy receipt
       return r;

    }

    public Receipt sell(int transId, int userId, PurchaseOrder order) throws CommException, RemoteException {
        debugLog.info("sell: start");

        UserInfo userInfo = findUserInfo(userId);

        //
        // Create and execute the Sell task
        //
        SellDBTask dbTask = new SellDBTask(transId, userId, order, userInfo);
        long startTime = System.currentTimeMillis();
        synchronized (dbTask) {

            // execute() method returns when the task has been added to the queue
            this.threadPool.execute(dbTask);
            long queueSize = this.taskQueue.size();
            perfLog.info("Queue Size: " + queueSize);
            myStats.addQueueSize(queueSize);

            // Wait for the task to complete
            try {
                dbTask.wait();
                addStats(startTime, dbTask.getReadQryCount(), dbTask.getWriteQryCount(),
                         dbTask.getQueueTime(), dbTask.getExeTime());
            } catch (InterruptedException ignored) {
            }
        }

        // If an error occured, throw the exception.
        dbTask.throwIfError();

        // Otherwise, get the receipt
        Receipt r = dbTask.getReceipt();
        debugLog.info("Buy complete");

        debugLog.info("sell: end of sell");

        logger.accountAdd(serverId, transId, userInfo.username, order.total.doubleValue());

        return r;

    }

    public Receipt addFunds(int transId, int userId, float amount) throws CommException, RemoteException {
        debugLog.info("add: start of add");

        UserInfo userInfo = findUserInfo(userId);

        //
        // Create and execute the Sell task
        //
        AddFundsDBTask dbTask = new AddFundsDBTask(transId, userId, amount, userInfo);
        long startTime = System.currentTimeMillis();
        synchronized (dbTask) {

            // execute() method returns when the task has been added to the queue
            this.threadPool.execute(dbTask);
            long queueSize = this.taskQueue.size();
            perfLog.info("Queue Size: " + queueSize);
            myStats.addQueueSize(queueSize);

            // Wait for the task to complete
            try {
                dbTask.wait();
                addStats(startTime, dbTask.getReadQryCount(), dbTask.getWriteQryCount(),
                         dbTask.getQueueTime(), dbTask.getExeTime());
            } catch (InterruptedException ignored) {
            }
        }

        // If an error occured, throw the exception.
        dbTask.throwIfError();

        // Otherwise, get the receipt
        Receipt r = dbTask.getReceipt();
        debugLog.info("Buy complete");

        logger.accountAdd(serverId, transId, userInfo.username, r.order.total.doubleValue());

        return r;
    }


    /**
     * @return new balance
     */
    public int addBuyTrigger(int userId, Trigger trigger) throws CommException, RemoteException {
        debugLog.info("DB addBuyTrigger");

        UserInfo userInfo = findUserInfo(userId);


        //
        // Create and execute the login task
        //
        AddBuyTriggerDBTask dbTask = new AddBuyTriggerDBTask(userId, userInfo, trigger, ++this.triggerId);
        long startTime = System.currentTimeMillis();
        synchronized (dbTask) {

            // execute() method returns when the task has been added to the queue
            this.threadPool.execute(dbTask);
            long queueSize = this.taskQueue.size();
            perfLog.info("Queue Size: " + queueSize);
            myStats.addQueueSize(queueSize);

            // Wait for the task to complete
            try {
                dbTask.wait();
                addStats(startTime, dbTask.getReadQryCount(), dbTask.getWriteQryCount(),
                         dbTask.getQueueTime(), dbTask.getExeTime());
            } catch (InterruptedException ignored) {
            }
        }

        // If an error occured, throw the exception.
        dbTask.throwIfError();

        // Otherwise, get result
        Trigger newTrigger = dbTask.getTrigger();

        buyTriggers.put(new Integer(newTrigger.id), new Trigger(newTrigger));
        return newTrigger.id;
    }

    public void cancelBuyTrigger(int userId, int triggerId) throws CommException, RemoteException {

        BigDecimal bal;
        Integer id = new Integer(triggerId);
        Trigger trigger = (Trigger)buyTriggers.get(id);
        if (trigger == null) {
            throw new CommException("DBServer: Trigger not found");
        }
        buyTriggers.remove(id);

        //this should be a buy trigger so the dollar amount is the valid trigger amount
        if (Trigger.BUY != trigger.kind)
            throw new CommException("cancelBuyTrigger: Wrong trigger type!");

        UserInfo userInfo = findUserInfo(userId);

        //
        // Create and execute the login task
        //
        CancelBuyTriggerDBTask dbTask = new CancelBuyTriggerDBTask(userId, userInfo, trigger);
        long startTime = System.currentTimeMillis();
        synchronized (dbTask) {

            // execute() method returns when the task has been added to the queue
            this.threadPool.execute(dbTask);
            long queueSize = this.taskQueue.size();
            perfLog.info("Queue Size: " + queueSize);
            myStats.addQueueSize(queueSize);

            // Wait for the task to complete
            try {
                dbTask.wait();
                addStats(startTime, dbTask.getReadQryCount(), dbTask.getWriteQryCount(),
                         dbTask.getQueueTime(), dbTask.getExeTime());
            } catch (InterruptedException ignored) {
            }
        }

        // If an error occured, throw the exception.
        dbTask.throwIfError();
    }

    public int addSellTrigger(int userId, Trigger trigger) throws CommException, RemoteException {
        UserInfo userInfo = findUserInfo(userId);

        //
        // Create and execute the login task
        //
        AddSellTriggerDBTask dbTask = new AddSellTriggerDBTask(userId, userInfo, trigger, ++this.triggerId);
        long startTime = System.currentTimeMillis();
        synchronized (dbTask) {

            // execute() method returns when the task has been added to the queue
            this.threadPool.execute(dbTask);
            long queueSize = this.taskQueue.size();
            perfLog.info("Queue Size: " + queueSize);
            myStats.addQueueSize(queueSize);

            // Wait for the task to complete
            try {
                dbTask.wait();
                addStats(startTime, dbTask.getReadQryCount(), dbTask.getWriteQryCount(),
                         dbTask.getQueueTime(), dbTask.getExeTime());
            } catch (InterruptedException ignored) {
            }
        }

        // If an error occured, throw the exception.
        dbTask.throwIfError();

        trigger = dbTask.getTrigger();
        sellTriggers.put(new Integer(trigger.id), new Trigger(trigger));
        return trigger.id;
    }

    public void cancelSellTrigger(int userId, int triggerId) throws CommException, RemoteException {

        UserInfo userInfo = findUserInfo(userId);

        Integer id = new Integer(triggerId);
        Trigger trigger = (Trigger)sellTriggers.get(id);
        if (trigger == null) {
            throw new CommException("DBServer: Trigger not found");
        }

        buyTriggers.remove(id);

        //this should be a buy trigger so the dollar amount is the valid trigger amount
        if (Trigger.SELL != trigger.kind)
            throw new CommException("cancelSellTrigger: Wrong trigger type!");


        //
        // Create and execute the login task
        //
        CancelSellTriggerDBTask dbTask = new CancelSellTriggerDBTask(userId, userInfo, trigger);
        long startTime = System.currentTimeMillis();
        synchronized (dbTask) {

            // execute() method returns when the task has been added to the queue
            this.threadPool.execute(dbTask);
            long queueSize = this.taskQueue.size();
            perfLog.info("Queue Size: " + queueSize);
            myStats.addQueueSize(queueSize);

            // Wait for the task to complete
            try {
                dbTask.wait();
                addStats(startTime, dbTask.getReadQryCount(), dbTask.getWriteQryCount(),
                         dbTask.getQueueTime(), dbTask.getExeTime());
            } catch (InterruptedException ignored) {
            }
        }

        // If an error occured, throw the exception.
        dbTask.throwIfError();

    }


    //
    // StatsCollector Interface
    //


    public void beginRun() throws RemoteException {
        myStats.start();
    }

    public void endRun() throws RemoteException {
        myStats.stop();
    }

    public Stats getStats() throws RemoteException {
        return myStats.clone();
    }


    //*******************************************************
    /*
    protected static int getResultSetSize(ResultSet resultSet) {
        int size = -1;

        long startTime = 0;
        if (debugLog.isDebugEnabled()) {
            startTime = System.currentTimeMillis();
        }

        try {
            resultSet.last();
            size = resultSet.getRow();
            resultSet.beforeFirst();
        } catch (SQLException err) {
            debugLog.error("getResultSetSize error", err);
            return size;
        }

        if (debugLog.isDebugEnabled()) {
            debugLog.debug("getRestulSetSize size:" + size + ", time:" + (startTime - System.currentTimeMillis()));
        }
        return size;
    }
    */


    //******************
    // private functions
    //******************
    private void addStats(long time, int readCount, int writeCount, long queueTime, long exeTime) {
        if (perfLog.isDebugEnabled()) {
            long currentTime = System.currentTimeMillis();
            perfLog.debug("Query Time: " + (currentTime - time) + " Queue Time: " + queueTime + " Exe Time: " + exeTime);
        }
        myStats.addQuery(time);
        myStats.incReadQueryCount(readCount);
        myStats.incWriteQueryCount(writeCount);
        myStats.addQueueTime((int)queueTime);
        myStats.addExeTime((int)exeTime);
    }


    private static BigDecimal roundForDB(float num) {
        double dbl = (double)num;
        BigDecimal bal = new BigDecimal(dbl);
        bal = bal.setScale(2, RoundingMode.HALF_EVEN);
        return bal;
    }


    //functions to execute and return the results of the given query string
    private int exec_update(String qry) throws SQLException {
        int result = 0;
        DBConnection2 db = new DBConnection2();
        try {
            db.openConnection();
            result = db.execUpdate(qry);
        } catch (SQLException sqle) {
            db.closeConnection();
            throw new SQLException("exec_update failed" + sqle);
        }
        db.closeConnection();

        return result;
    }

    private ResultSet exec_qry(String qry) throws SQLException {
        ResultSet result = null;
        DBConnection2 db = new DBConnection2();
        try {
            debugLog.info("exec_qry running: " + qry);
            db.openConnection();
            result = db.execQuery(qry);
            debugLog.info("exec_qry qry complete");
        } catch (SQLException sqle) {
            db.closeConnection();
            throw new SQLException("exec_qry failed" + sqle);
        }
        db.closeConnection();
        return result;
    }



}



